"use client" import * as React from 'react' import { ChevronRight, Folder, FileText, FileJson, FileCode2, File as FileIcon, Lock, GitBranch, Coins, Copy } from 'lucide-react' import { Button } from '@open-mercato/ui/primitives/button' import { Input } from '@open-mercato/ui/primitives/input' import { Tag } from '@open-mercato/ui/primitives/tag' import { Spinner } from '@open-mercato/ui/primitives/spinner' import { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control' import { ErrorMessage } from '@open-mercato/ui/backend/detail' import { MarkdownContent } from '@open-mercato/ui/backend/markdown/MarkdownContent' import { flash } from '@open-mercato/ui/backend/FlashMessages' import { apiCall } from '@open-mercato/ui/backend/utils/apiCall' import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context' import { formatNumber, type AgentDetailView } from '../../../../components/types' import { buildFileTree, filterTree, fileKind, FILE_KIND_LABEL, formatBytes, splitFrontmatter, utf8Bytes, type AgentFilesResponse, type AgentSourceFile, type FileTreeNode, } from './filesShared' type FilesTabProps = { agentId: string agent: AgentDetailView active: boolean } function FileGlyph({ name }: { name: string }) { const kind = fileKind(name) const className = 'size-4 shrink-0 text-muted-foreground' if (kind === 'md') return if (kind === 'json') return if (kind === 'ts') return return } function CodeView({ content }: { content: string }) { const body = content.replace(/\n$/, '') const lineCount = body.split('\n').length const gutter = Array.from({ length: lineCount }, (_, i) => i + 1).join('\n') return (
{gutter}
{body}
) } function FileReader({ file }: { file: AgentSourceFile }) { const t = useT() const locale = useLocale() const [view, setView] = React.useState<'preview' | 'raw'>('preview') React.useEffect(() => { setView('preview') }, [file.path]) const kind = fileKind(file.path.split('/').pop() as string) const segments = file.path.split('/') const leaf = segments[segments.length - 1] const lineCount = file.content.replace(/\n$/, '').split('\n').length const meta = file.inContext ? `${FILE_KIND_LABEL[kind]} · ${lineCount} ${t('agent_orchestrator.files.lines', 'lines')} · ${formatBytes(utf8Bytes(file.content))} · ~${formatNumber(file.tokens, locale)} ${t('agent_orchestrator.files.tokens', 'tokens')}` : `${FILE_KIND_LABEL[kind]} · ${lineCount} ${t('agent_orchestrator.files.lines', 'lines')} · ${formatBytes(utf8Bytes(file.content))} · ${t('agent_orchestrator.files.notInContext', 'not in model context')}` const copy = () => { if (typeof navigator === 'undefined' || !navigator.clipboard) return navigator.clipboard.writeText(file.content).then( () => flash(t('agent_orchestrator.files.copied', 'File copied to clipboard.'), 'success'), () => flash(t('agent_orchestrator.files.copyFailed', 'Could not copy to clipboard.'), 'error'), ) } const { frontmatter, body } = kind === 'md' ? splitFrontmatter(file.content) : { frontmatter: null, body: file.content } return (
{agentRoot(segments)} {leaf}
{meta} {kind === 'md' ? ( setView(value as 'preview' | 'raw')} aria-label={t('agent_orchestrator.files.viewToggle', 'File view')}> {t('agent_orchestrator.files.preview', 'Preview')} {t('agent_orchestrator.files.raw', 'Raw')} ) : null}
{kind === 'md' && view === 'preview' ? (
{frontmatter && frontmatter.length ? (
{frontmatter.map((row) => (
{row.key}
{row.value}
))}
) : null}
) : ( )}
) } function agentRoot(segments: string[]): string { if (segments.length <= 1) return '' return `${segments.slice(0, -1).join('/')}/` } function TreeNode({ node, depth, selectedPath, onSelect, locale, }: { node: FileTreeNode depth: number selectedPath: string | null onSelect: (path: string) => void locale: string }) { const t = useT() const [open, setOpen] = React.useState(true) const pad = { paddingLeft: `${6 + depth * 16}px` } if (node.type === 'file') { const selected = selectedPath === node.path return ( ) } return (
{open ? (
{(node.children ?? []).map((child) => ( ))}
) : null}
) } function DefinitionOverview({ agent }: { agent: AgentDetailView }) { const t = useT() const locale = useLocale() const defaultValue = t('agent_orchestrator.agentDetail.defaultValue', 'Default') const items: Array<{ key: string; value: string; mono?: boolean }> = [ { key: t('agent_orchestrator.agentDetail.config.runtimeKind', 'Runtime'), value: `${agent.runtime} · ${t('agent_orchestrator.files.fileDefined', 'file-defined')}`, mono: true }, { key: t('agent_orchestrator.agentDetail.fields.provider', 'Provider'), value: agent.defaultProvider ?? defaultValue, mono: true }, { key: t('agent_orchestrator.agentDetail.fields.model', 'Model'), value: agent.defaultModel ?? defaultValue, mono: true }, { key: t('agent_orchestrator.agentDetail.fields.maxSteps', 'Max steps'), value: agent.loopMaxSteps != null ? String(agent.loopMaxSteps) : defaultValue }, { key: t('agent_orchestrator.agentDetail.config.resultKind', 'Result kind'), value: agent.resultKind }, ] if (agent.tokenUsage) { items.push({ key: t('agent_orchestrator.files.contextEstimate', 'Context estimate'), value: `~${formatNumber(agent.tokenUsage.total, locale)} ${t('agent_orchestrator.files.tokens', 'tokens')}` }) } return (
{items.map((item) => (
{item.key} {item.value}
))} {agent.tools.length ? : null} {agent.skills.length ? : null} {agent.subAgents.length ? : null}
) } function ChipGroup({ label, values }: { label: string; values: string[] }) { return (
{label} {values.map((value) => {value})}
) } export function FilesTab({ agentId, agent, active }: FilesTabProps) { const t = useT() const locale = useLocale() const [files, setFiles] = React.useState(null) const [state, setState] = React.useState<'idle' | 'loading' | 'ready' | 'error'>('idle') const [selectedPath, setSelectedPath] = React.useState(null) const [filter, setFilter] = React.useState('') // Dedupe by agent id (survives React's dev double-invoke) instead of a // cleanup-cancel guard, which would drop the sole in-flight fetch on remount. const loadedForRef = React.useRef(null) React.useEffect(() => { if (!active || loadedForRef.current === agentId) return loadedForRef.current = agentId setState('loading') apiCall(`/api/agent_orchestrator/agents/${encodeURIComponent(agentId)}/files`) .then((call) => { if (loadedForRef.current !== agentId) return if (!call.ok || !call.result) { setState('error') return } const items = call.result.files ?? [] setFiles(items) const preferred = items.find((f) => f.path === 'AGENT.md') ?? items[0] setSelectedPath(preferred ? preferred.path : null) setState('ready') }) .catch(() => { if (loadedForRef.current === agentId) setState('error') }) }, [active, agentId]) const tree = React.useMemo(() => (files ? buildFileTree(files) : []), [files]) const visibleTree = React.useMemo(() => filterTree(tree, filter), [tree, filter]) const selectedFile = React.useMemo( () => (files && selectedPath ? files.find((f) => f.path === selectedPath) ?? null : null), [files, selectedPath], ) const contextTotal = agent.tokenUsage?.total ?? (files ? files.filter((f) => f.inContext).reduce((sum, f) => sum + f.tokens, 0) : 0) return (
{agent.id.includes('.') ? agent.id.split('.').slice(-1)[0] : agent.id}
{t('agent_orchestrator.files.definition', 'definition')} {files ? {t('agent_orchestrator.files.fileCount', undefined, { count: files.length })} : null}
~{formatNumber(contextTotal, locale)} {t('agent_orchestrator.files.tokens', 'tokens')} {t('agent_orchestrator.files.readOnly', 'Read-only')}
{state === 'loading' || state === 'idle' ? (
{t('agent_orchestrator.files.loading', 'Loading files…')}
) : state === 'error' ? (
) : (
setFilter(e.target.value)} placeholder={t('agent_orchestrator.files.filter', 'Filter files…')} className="h-8" />
{t('agent_orchestrator.files.tokenLegend', 'Numbers are the o200k_base token estimate each file adds to context; sample inputs are not counted.')}
{visibleTree.length ? ( visibleTree.map((node) => ( )) ) : (

{t('agent_orchestrator.files.noMatches', 'No files match your filter.')}

)}
{selectedFile ? : (
{t('agent_orchestrator.files.selectPrompt', 'Select a file to read it.')}
)}
)}
) } export default FilesTab